/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.dx.dex.file;

import java.util.Collection;

import com.android.dx.rop.annotation.Annotation;
import com.android.dx.rop.annotation.NameValuePair;
import com.android.dx.rop.cst.Constant;
import com.android.dx.rop.cst.CstAnnotation;
import com.android.dx.rop.cst.CstArray;
import com.android.dx.rop.cst.CstBoolean;
import com.android.dx.rop.cst.CstByte;
import com.android.dx.rop.cst.CstChar;
import com.android.dx.rop.cst.CstDouble;
import com.android.dx.rop.cst.CstEnumRef;
import com.android.dx.rop.cst.CstFieldRef;
import com.android.dx.rop.cst.CstFloat;
import com.android.dx.rop.cst.CstInteger;
import com.android.dx.rop.cst.CstKnownNull;
import com.android.dx.rop.cst.CstLiteralBits;
import com.android.dx.rop.cst.CstLong;
import com.android.dx.rop.cst.CstMethodRef;
import com.android.dx.rop.cst.CstShort;
import com.android.dx.rop.cst.CstString;
import com.android.dx.rop.cst.CstType;
import com.android.dx.util.AnnotatedOutput;
import com.android.dx.util.Hex;

/**
 * Handler for writing out {@code encoded_values} and parts thereof.
 */
public final class ValueEncoder {

	/** annotation value type constant: {@code byte} */
	private static final int VALUE_BYTE = 0x00;

	/** annotation value type constant: {@code short} */
	private static final int VALUE_SHORT = 0x02;

	/** annotation value type constant: {@code char} */
	private static final int VALUE_CHAR = 0x03;

	/** annotation value type constant: {@code int} */
	private static final int VALUE_INT = 0x04;

	/** annotation value type constant: {@code long} */
	private static final int VALUE_LONG = 0x06;

	/** annotation value type constant: {@code float} */
	private static final int VALUE_FLOAT = 0x10;

	/** annotation value type constant: {@code double} */
	private static final int VALUE_DOUBLE = 0x11;

	/** annotation value type constant: {@code string} */
	private static final int VALUE_STRING = 0x17;

	/** annotation value type constant: {@code type} */
	private static final int VALUE_TYPE = 0x18;

	/** annotation value type constant: {@code field} */
	private static final int VALUE_FIELD = 0x19;

	/** annotation value type constant: {@code method} */
	private static final int VALUE_METHOD = 0x1a;

	/** annotation value type constant: {@code enum} */
	private static final int VALUE_ENUM = 0x1b;

	/** annotation value type constant: {@code array} */
	private static final int VALUE_ARRAY = 0x1c;

	/** annotation value type constant: {@code annotation} */
	private static final int VALUE_ANNOTATION = 0x1d;

	/** annotation value type constant: {@code null} */
	private static final int VALUE_NULL = 0x1e;

	/** annotation value type constant: {@code boolean} */
	private static final int VALUE_BOOLEAN = 0x1f;

	/** {@code non-null;} file being written */
	private final DexFile file;

	/** {@code non-null;} output stream to write to */
	private final AnnotatedOutput out;

	/**
	 * Construct an instance.
	 * 
	 * @param file
	 *            {@code non-null;} file being written
	 * @param out
	 *            {@code non-null;} output stream to write to
	 */
	public ValueEncoder(DexFile file, AnnotatedOutput out) {
		if (file == null) {
			throw new NullPointerException("file == null");
		}

		if (out == null) {
			throw new NullPointerException("out == null");
		}

		this.file = file;
		this.out = out;
	}

	/**
	 * Writes out the encoded form of the given constant.
	 * 
	 * @param cst
	 *            {@code non-null;} the constant to write
	 */
	public void writeConstant(Constant cst) {
		int type = constantToValueType(cst);
		int arg;

		switch (type) {
		case VALUE_BYTE:
		case VALUE_SHORT:
		case VALUE_INT:
		case VALUE_LONG: {
			long value = ((CstLiteralBits) cst).getLongBits();
			writeSignedIntegralValue(type, value);
			break;
		}
		case VALUE_CHAR: {
			long value = ((CstLiteralBits) cst).getLongBits();
			writeUnsignedIntegralValue(type, value);
			break;
		}
		case VALUE_FLOAT: {
			// Shift value left 32 so that right-zero-extension works.
			long value = ((CstFloat) cst).getLongBits() << 32;
			writeRightZeroExtendedValue(type, value);
			break;
		}
		case VALUE_DOUBLE: {
			long value = ((CstDouble) cst).getLongBits();
			writeRightZeroExtendedValue(type, value);
			break;
		}
		case VALUE_STRING: {
			int index = file.getStringIds().indexOf((CstString) cst);
			writeUnsignedIntegralValue(type, (long) index);
			break;
		}
		case VALUE_TYPE: {
			int index = file.getTypeIds().indexOf((CstType) cst);
			writeUnsignedIntegralValue(type, (long) index);
			break;
		}
		case VALUE_FIELD: {
			int index = file.getFieldIds().indexOf((CstFieldRef) cst);
			writeUnsignedIntegralValue(type, (long) index);
			break;
		}
		case VALUE_METHOD: {
			int index = file.getMethodIds().indexOf((CstMethodRef) cst);
			writeUnsignedIntegralValue(type, (long) index);
			break;
		}
		case VALUE_ENUM: {
			CstFieldRef fieldRef = ((CstEnumRef) cst).getFieldRef();
			int index = file.getFieldIds().indexOf(fieldRef);
			writeUnsignedIntegralValue(type, (long) index);
			break;
		}
		case VALUE_ARRAY: {
			out.writeByte(type);
			writeArray((CstArray) cst, false);
			break;
		}
		case VALUE_ANNOTATION: {
			out.writeByte(type);
			writeAnnotation(((CstAnnotation) cst).getAnnotation(), false);
			break;
		}
		case VALUE_NULL: {
			out.writeByte(type);
			break;
		}
		case VALUE_BOOLEAN: {
			int value = ((CstBoolean) cst).getIntBits();
			out.writeByte(type | (value << 5));
			break;
		}
		default: {
			throw new RuntimeException("Shouldn't happen");
		}
		}
	}

	/**
	 * Gets the value type for the given constant.
	 * 
	 * @param cst
	 *            {@code non-null;} the constant
	 * @return the value type; one of the {@code VALUE_*} constants defined by
	 *         this class
	 */
	private static int constantToValueType(Constant cst) {
		/*
		 * TODO: Constant should probable have an associated enum, so this can
		 * be a switch().
		 */
		if (cst instanceof CstByte) {
			return VALUE_BYTE;
		} else if (cst instanceof CstShort) {
			return VALUE_SHORT;
		} else if (cst instanceof CstChar) {
			return VALUE_CHAR;
		} else if (cst instanceof CstInteger) {
			return VALUE_INT;
		} else if (cst instanceof CstLong) {
			return VALUE_LONG;
		} else if (cst instanceof CstFloat) {
			return VALUE_FLOAT;
		} else if (cst instanceof CstDouble) {
			return VALUE_DOUBLE;
		} else if (cst instanceof CstString) {
			return VALUE_STRING;
		} else if (cst instanceof CstType) {
			return VALUE_TYPE;
		} else if (cst instanceof CstFieldRef) {
			return VALUE_FIELD;
		} else if (cst instanceof CstMethodRef) {
			return VALUE_METHOD;
		} else if (cst instanceof CstEnumRef) {
			return VALUE_ENUM;
		} else if (cst instanceof CstArray) {
			return VALUE_ARRAY;
		} else if (cst instanceof CstAnnotation) {
			return VALUE_ANNOTATION;
		} else if (cst instanceof CstKnownNull) {
			return VALUE_NULL;
		} else if (cst instanceof CstBoolean) {
			return VALUE_BOOLEAN;
		} else {
			throw new RuntimeException("Shouldn't happen");
		}
	}

	/**
	 * Writes out the encoded form of the given array, that is, as an
	 * {@code encoded_array} and not including a {@code value_type} prefix. If
	 * the output stream keeps (debugging) annotations and {@code topLevel} is
	 * {@code true}, then this method will write (debugging) annotations.
	 * 
	 * @param array
	 *            {@code non-null;} array instance to write
	 * @param topLevel
	 *            {@code true} iff the given annotation is the top-level
	 *            annotation or {@code false} if it is a sub-annotation of some
	 *            other annotation
	 */
	public void writeArray(CstArray array, boolean topLevel) {
		boolean annotates = topLevel && out.annotates();
		CstArray.List list = ((CstArray) array).getList();
		int size = list.size();

		if (annotates) {
			out.annotate("  size: " + Hex.u4(size));
		}

		out.writeUleb128(size);

		for (int i = 0; i < size; i++) {
			Constant cst = list.get(i);
			if (annotates) {
				out.annotate("  [" + Integer.toHexString(i) + "] "
						+ constantToHuman(cst));
			}
			writeConstant(cst);
		}

		if (annotates) {
			out.endAnnotation();
		}
	}

	/**
	 * Writes out the encoded form of the given annotation, that is, as an
	 * {@code encoded_annotation} and not including a {@code value_type} prefix.
	 * If the output stream keeps (debugging) annotations and {@code topLevel}
	 * is {@code true}, then this method will write (debugging) annotations.
	 * 
	 * @param annotation
	 *            {@code non-null;} annotation instance to write
	 * @param topLevel
	 *            {@code true} iff the given annotation is the top-level
	 *            annotation or {@code false} if it is a sub-annotation of some
	 *            other annotation
	 */
	public void writeAnnotation(Annotation annotation, boolean topLevel) {
		boolean annotates = topLevel && out.annotates();
		StringIdsSection stringIds = file.getStringIds();
		TypeIdsSection typeIds = file.getTypeIds();

		CstType type = annotation.getType();
		int typeIdx = typeIds.indexOf(type);

		if (annotates) {
			out.annotate("  type_idx: " + Hex.u4(typeIdx) + " // "
					+ type.toHuman());
		}

		out.writeUleb128(typeIds.indexOf(annotation.getType()));

		Collection<NameValuePair> pairs = annotation.getNameValuePairs();
		int size = pairs.size();

		if (annotates) {
			out.annotate("  size: " + Hex.u4(size));
		}

		out.writeUleb128(size);

		int at = 0;
		for (NameValuePair pair : pairs) {
			CstString name = pair.getName();
			int nameIdx = stringIds.indexOf(name);
			Constant value = pair.getValue();

			if (annotates) {
				out.annotate(0, "  elements[" + at + "]:");
				at++;
				out.annotate("    name_idx: " + Hex.u4(nameIdx) + " // "
						+ name.toHuman());
			}

			out.writeUleb128(nameIdx);

			if (annotates) {
				out.annotate("    value: " + constantToHuman(value));
			}

			writeConstant(value);
		}

		if (annotates) {
			out.endAnnotation();
		}
	}

	/**
	 * Gets the colloquial type name and human form of the type of the given
	 * constant, when used as an encoded value.
	 * 
	 * @param cst
	 *            {@code non-null;} the constant
	 * @return {@code non-null;} its type name and human form
	 */
	public static String constantToHuman(Constant cst) {
		int type = constantToValueType(cst);

		if (type == VALUE_NULL) {
			return "null";
		}

		StringBuilder sb = new StringBuilder();

		sb.append(cst.typeName());
		sb.append(' ');
		sb.append(cst.toHuman());

		return sb.toString();
	}

	/**
	 * Helper for {@link #writeConstant}, which writes out the value for any
	 * signed integral type.
	 * 
	 * @param type
	 *            the type constant
	 * @param value
	 *            {@code long} bits of the value
	 */
	private void writeSignedIntegralValue(int type, long value) {
		/*
		 * Figure out how many bits are needed to represent the value, including
		 * a sign bit: The bit count is subtracted from 65 and not 64 to account
		 * for the sign bit. The xor operation has the effect of leaving
		 * non-negative values alone and unary complementing negative values (so
		 * that a leading zero count always returns a useful number for our
		 * present purpose).
		 */
		int requiredBits = 65 - Long
				.numberOfLeadingZeros(value ^ (value >> 63));

		// Round up the requiredBits to a number of bytes.
		int requiredBytes = (requiredBits + 0x07) >> 3;

		/*
		 * Write the header byte, which includes the type and requiredBytes - 1.
		 */
		out.writeByte(type | ((requiredBytes - 1) << 5));

		// Write the value, per se.
		while (requiredBytes > 0) {
			out.writeByte((byte) value);
			value >>= 8;
			requiredBytes--;
		}
	}

	/**
	 * Helper for {@link #writeConstant}, which writes out the value for any
	 * unsigned integral type.
	 * 
	 * @param type
	 *            the type constant
	 * @param value
	 *            {@code long} bits of the value
	 */
	private void writeUnsignedIntegralValue(int type, long value) {
		// Figure out how many bits are needed to represent the value.
		int requiredBits = 64 - Long.numberOfLeadingZeros(value);
		if (requiredBits == 0) {
			requiredBits = 1;
		}

		// Round up the requiredBits to a number of bytes.
		int requiredBytes = (requiredBits + 0x07) >> 3;

		/*
		 * Write the header byte, which includes the type and requiredBytes - 1.
		 */
		out.writeByte(type | ((requiredBytes - 1) << 5));

		// Write the value, per se.
		while (requiredBytes > 0) {
			out.writeByte((byte) value);
			value >>= 8;
			requiredBytes--;
		}
	}

	/**
	 * Helper for {@link #writeConstant}, which writes out a right-zero-extended
	 * value.
	 * 
	 * @param type
	 *            the type constant
	 * @param value
	 *            {@code long} bits of the value
	 */
	private void writeRightZeroExtendedValue(int type, long value) {
		// Figure out how many bits are needed to represent the value.
		int requiredBits = 64 - Long.numberOfTrailingZeros(value);
		if (requiredBits == 0) {
			requiredBits = 1;
		}

		// Round up the requiredBits to a number of bytes.
		int requiredBytes = (requiredBits + 0x07) >> 3;

		// Scootch the first bits to be written down to the low-order bits.
		value >>= 64 - (requiredBytes * 8);

		/*
		 * Write the header byte, which includes the type and requiredBytes - 1.
		 */
		out.writeByte(type | ((requiredBytes - 1) << 5));

		// Write the value, per se.
		while (requiredBytes > 0) {
			out.writeByte((byte) value);
			value >>= 8;
			requiredBytes--;
		}
	}

	/**
	 * Helper for {@code addContents()} methods, which adds contents for a
	 * particular {@link Annotation}, calling itself recursively should it
	 * encounter a nested annotation.
	 * 
	 * @param file
	 *            {@code non-null;} the file to add to
	 * @param annotation
	 *            {@code non-null;} the annotation to add contents for
	 */
	public static void addContents(DexFile file, Annotation annotation) {
		TypeIdsSection typeIds = file.getTypeIds();
		StringIdsSection stringIds = file.getStringIds();

		typeIds.intern(annotation.getType());

		for (NameValuePair pair : annotation.getNameValuePairs()) {
			stringIds.intern(pair.getName());
			addContents(file, pair.getValue());
		}
	}

	/**
	 * Helper for {@code addContents()} methods, which adds contents for a
	 * particular constant, calling itself recursively should it encounter a
	 * {@link CstArray} and calling {@link #addContents(DexFile,Annotation)}
	 * recursively should it encounter a {@link CstAnnotation}.
	 * 
	 * @param file
	 *            {@code non-null;} the file to add to
	 * @param cst
	 *            {@code non-null;} the constant to add contents for
	 */
	public static void addContents(DexFile file, Constant cst) {
		if (cst instanceof CstAnnotation) {
			addContents(file, ((CstAnnotation) cst).getAnnotation());
		} else if (cst instanceof CstArray) {
			CstArray.List list = ((CstArray) cst).getList();
			int size = list.size();
			for (int i = 0; i < size; i++) {
				addContents(file, list.get(i));
			}
		} else {
			file.internIfAppropriate(cst);
		}
	}
}
